package com.zhima.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhima.constant.ContestRedisConstant;
import com.zhima.dto.ContestBasicInfoDTO;
import com.zhima.dto.FeignQuestionBasicInfoDTO;
import com.zhima.dto.UserDTO;
import com.zhima.dto.UserSimpleInfo;
import com.zhima.entity.OjContest;
import com.zhima.entity.OjQuestionBindContest;
import com.zhima.exception.ResourceNotFoundException;
import com.zhima.exception.ZhimaOjException;
import com.zhima.feignclient.QuestionFeignClient;
import com.zhima.feignclient.UserFeignClient;
import com.zhima.mapper.OjContestMapper;
import com.zhima.service.OjContestService;
import com.zhima.service.OjQuestionBindContestService;
import com.zhima.util.AuthorizationUtil;
import com.zhima.util.PageUtil;
import com.zhima.util.UserHolder;
import com.zhima.vo.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

import static com.zhima.constant.ContestRedisConstant.*;
import static com.zhima.exception.ErrorCode.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author zhima
 * @since 2024-01-18
 */
@Service
public class OjContestServiceImpl extends ServiceImpl<OjContestMapper, OjContest> implements OjContestService {
    private static final String RANDOM_BASE_STRING = "abcdefghijklmnopqrstuvwxyz1234567890";
    @Autowired
    private QuestionFeignClient questionFeignClient;
    @Autowired
    private OjQuestionBindContestService ojQuestionBindContestService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private OjContestMapper ojContestMapper;
    @Autowired
    private UserFeignClient userFeignClient;

    @Override
    public String createNewContest(Long userId, ContestBasicInfoDTO contestBasicInfoDTO) {
        OjContest ojContest = BeanUtil.copyProperties(contestBasicInfoDTO, OjContest.class);
        ojContest.setDuration(DateUtil.between(ojContest.getBeginTime(), ojContest.getEndTime(), DateUnit.MINUTE));
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 4; i++) {
            stringBuilder.append(RandomUtil.randomChar(RANDOM_BASE_STRING));
        }
        ojContest.setInviteCode(stringBuilder.toString());
        ojContest.setHolderId(userId);
        ojContest.setHolderName(userFeignClient.basicInfoById(userId).getUserName());
        ojContest.setApplyNum(0);
        ojContest.setQuestionNum(0);
        save(ojContest);
        // 标志比赛没有被填充
        stringRedisTemplate.opsForValue().set(CONTEST_INIT_PREFIX + ojContest.getId(), "");
        return ojContest.getId().toString();
    }

    @Override
    public boolean deleteContestById(Long contestId) {
        // 删除Redis中的比赛信息
        deleteRedisContest(contestId);
        return removeById(contestId);
    }

    private void deleteRedisContest(Long contestId) {
        Set<String> members = stringRedisTemplate.opsForSet().members(CONTEST_APPLY_PREFIX + contestId);
        // 报名信息
        stringRedisTemplate.delete(CONTEST_APPLY_PREFIX + contestId);
        // 初始化标志
        stringRedisTemplate.delete(CONTEST_INIT_PREFIX + contestId);
        // AC标记
        stringRedisTemplate.delete(CONTEST_AC_PREFIX + contestId);
        // 分数榜
        stringRedisTemplate.delete(CONTEST_SCORE_PREFIX + contestId);
        // 榜单信息
        stringRedisTemplate.delete(CONTEST_SCOREBOARD_PREFIX + contestId);
        // 首次AC榜
        stringRedisTemplate.delete(CONTEST_FIRST_AC_USER_PREFIX + contestId);
        if (members != null && !members.isEmpty()) {
            for (String member : members) {
                // 用户提交标志
                stringRedisTemplate.delete(CONTEST_SUBMIT_PREFIX + contestId + ":" + member);
            }
        }
    }

    @Override
    public ContestShowVO getContestById(UserDTO userDTO, Long contestId) {
        OjContest ojContest = getById(contestId);
        if (ojContest == null) {
            throw new ResourceNotFoundException();
        }
        Date now = new Date();
        boolean isBegin = ojContest.getBeginTime().before(now);
        if (isBegin && BooleanUtil.isTrue(stringRedisTemplate.delete(CONTEST_INIT_PREFIX + contestId))) {
            // 比赛开始了，并且没有被填充
            initContestRedis(contestId, ojContest);
        }
        ContestShowVO contestShowVO = BeanUtil.copyProperties(ojContest, ContestShowVO.class);
        // 登录用户是否报名
        contestShowVO.setIsApply(stringRedisTemplate.opsForSet().isMember(CONTEST_APPLY_PREFIX + contestId, userDTO.getId().toString()));
        contestShowVO.setIsBegin(isBegin);
        boolean isEnd = ojContest.getEndTime().before(now);
        contestShowVO.setIsEnd(isEnd);
        // 是否是创建者本人
        contestShowVO.setIsSelf(userDTO.getId().equals(ojContest.getHolderId()));
        if (!isBegin) {
            // 尚未开始
            return contestShowVO;
        }
        // 已经开始了
        // 比赛没有结束并且用户没有报名，直接返回，不允许查看比赛题目
        if (!isEnd && !contestShowVO.getIsApply()) {
            return contestShowVO;
        }
        // （比赛开始、用户报名）才可以看题和排行榜
        // 比赛结束都可以查看题目和排行榜
        List<OjQuestionBindContest> ojQuestionBindContests = ojQuestionBindContestService.listQuestionInfoByContestId(contestId);
        ContestQuestionVO[] contestQuestionVOS = new ContestQuestionVO[ojQuestionBindContests.size()];
        Map<ContestQuestionVO, UserDTO> questionFirstAcMap = new HashMap<>();
        // 本场比赛的首次AC榜
        contestShowVO.setQuestionFirstAcMap(questionFirstAcMap);
        contestShowVO.setContestQuestions(contestQuestionVOS);
        // 提交状态数组
        char[] submitBits = !contestShowVO.getIsApply() ? null : stringRedisTemplate.opsForValue().get(CONTEST_SUBMIT_PREFIX + contestId + ":" + userDTO.getId()).toCharArray();
        Map<Object, Object> questionFirstUserAcMap = stringRedisTemplate.opsForHash().entries(ContestRedisConstant.CONTEST_FIRST_AC_USER_PREFIX + contestId);
        // 得到原本设定的总分
        List<Integer> fullMarks = questionFeignClient.listFullMarks(ojQuestionBindContests.stream().map(OjQuestionBindContest::getQuestionId).collect(Collectors.toList()));
        for (int i = 0; i < contestQuestionVOS.length; i++) {
            ContestQuestionVO contestQuestionVO = new ContestQuestionVO();
            OjQuestionBindContest ojQuestionBindContest = ojQuestionBindContests.get(i);
            contestQuestionVO.setId(ojQuestionBindContest.getQuestionId().toString());
            FeignQuestionBasicInfoDTO feignQuestionBasicInfoDTO = questionFeignClient.queryQuestionBasicInfo(Long.parseLong(contestQuestionVO.getId()));
            contestQuestionVO.setTitle(feignQuestionBasicInfoDTO.getTitle());
            // 是1的话就是提交过了
            contestQuestionVO.setIsSubmit(contestShowVO.getIsApply() && submitBits[i] == '1');
            // 总分就是倍率乘以题目总分
            contestQuestionVO.setFullMark(fullMarks.get(i) * ojQuestionBindContest.getMagnification());
            contestQuestionVOS[i] = contestQuestionVO;
            Object firstAcQuestionUserId = questionFirstUserAcMap.get(contestQuestionVO.getId());
            // 如果不存在则置空
            questionFirstAcMap.put(contestQuestionVO, firstAcQuestionUserId == null ? null : userFeignClient.basicInfoById(Long.parseLong((String) firstAcQuestionUserId)));
        }
        return contestShowVO;
    }

    private void initContestRedis(Long contestId, OjContest ojContest) {
        // 需要填充 CONTEST_SUBMIT_PREFIX、CONTEST_AC_PREFIX
        Integer questionNum = ojContest.getQuestionNum();
        Set<String> userIdStrs = stringRedisTemplate.opsForSet().members(CONTEST_APPLY_PREFIX + contestId);
        if (userIdStrs == null || userIdStrs.isEmpty()) {
            return;
        }
        Map<String, String> submitMap = new HashMap<>();
        String stateStr = "0".repeat(questionNum);
        userIdStrs.forEach(userIdStr -> submitMap.put(CONTEST_SUBMIT_PREFIX + contestId + ":" + userIdStr, stateStr));
        // 填充CONTEST_SUBMIT_PREFIX
        stringRedisTemplate.opsForValue().multiSet(submitMap);
        // 填充CONTEST_AC_PREFIX
        stringRedisTemplate.opsForValue().set(CONTEST_AC_PREFIX + contestId, stateStr);
    }

    @Override
    public boolean applyContest(Long contestId, String inviteCode) {
        Long userId = UserHolder.get().getId();
        OjContest one = lambdaQuery().eq(OjContest::getId, contestId).one();
        // 校验邀请码
        if (StrUtil.isEmpty(inviteCode) || !one.getInviteCode().equals(inviteCode)) {
            throw new ZhimaOjException(INVITE_CODE_WRONG);
        }
        // 已经报名过了
        String userIdStr = userId.toString();
        if (BooleanUtil.isTrue(stringRedisTemplate.opsForSet().isMember(CONTEST_APPLY_PREFIX + contestId, userIdStr))) {
            throw new ZhimaOjException(NOT_ALLOW_APPLY_THE_SAME_CONTEST);
        }
        // 记录比赛报名
        stringRedisTemplate.opsForSet().add(CONTEST_APPLY_PREFIX + contestId, userIdStr);
        // 记录用户报名了此比赛
        stringRedisTemplate.opsForList().leftPush(CONTEST_USER_JOIN_PREFIX + userId, contestId.toString());
        // 增加报名人数
        return ojContestMapper.increaseApplyNumber(contestId);
    }

    @Override
    public ScoreInfo[] getContestScoreBoardByContestId(Long contestId) {
        UserDTO userDTO = UserHolder.get();
        OjContest contest = lambdaQuery().eq(OjContest::getId, contestId).select(OjContest::getQuestionNum).one();
        if (contest == null) {
            throw new ResourceNotFoundException();
        }
        ScoreInfo loginUserScoreInfo = new ScoreInfo(
                new ScoreInfo.SingleQuestionScore[contest.getQuestionNum()],
                userDTO.getUserName(),
                userDTO.getId().toString(),
                0,
                0
        );
        // 简略的排行榜信息
        // (用户id、用户名)、总分
        Set<ZSetOperations.TypedTuple<String>> simpleScoreBoard = stringRedisTemplate.opsForZSet().reverseRangeWithScores(ContestRedisConstant.CONTEST_SCOREBOARD_PREFIX + contestId, 0, -1);
        // 每个用户的具体得分情况
        Map<Object, Object> userScoreInfosMap = stringRedisTemplate.opsForHash().entries(ContestRedisConstant.CONTEST_SCORE_PREFIX + contestId);
        Iterator<ZSetOperations.TypedTuple<String>> iterator = simpleScoreBoard.iterator();
        int index = 1;
        // 第一个位置是登录用户的得分情况
        ScoreInfo[] rankScoreInfos = new ScoreInfo[simpleScoreBoard.size() + 1];
        while (iterator.hasNext()) {
            ZSetOperations.TypedTuple<String> simpleUserScore = iterator.next();
            ScoreInfo scoreInfo = new ScoreInfo();
            UserSimpleInfo user = JSON.parseObject(simpleUserScore.getValue(), UserSimpleInfo.class);
            String userIdStr = user.getId().toString();
            scoreInfo.setUserId(userIdStr);
            scoreInfo.setUserName(user.getUserName());
            scoreInfo.setSumScore(simpleUserScore.getScore().intValue());
            scoreInfo.setRank(index);
            // 直接从redis得到具体的得分情况
            scoreInfo.setSingleQuestionScores(
                    JSON.parseArray(userScoreInfosMap.get(userIdStr).toString(),
                                    ScoreInfo.SingleQuestionScore.class)
                            .toArray(new ScoreInfo.SingleQuestionScore[0])
            );
            rankScoreInfos[index++] = scoreInfo;
            if (user.getId().equals(userDTO.getId())) {
                loginUserScoreInfo = scoreInfo;
            }
        }
        rankScoreInfos[0] = loginUserScoreInfo;
        return rankScoreInfos;
    }


    @Override
    public boolean recordUserSubmit(Long contestId, Integer questionIndex, Long userId) {
        // 记录该用户对应的题提交过
        String key = CONTEST_SUBMIT_PREFIX + contestId + ":" + userId;
        char[] submitCharArray = stringRedisTemplate.opsForValue().get(key).toCharArray();
        submitCharArray[questionIndex] = '1';
        stringRedisTemplate.opsForValue().set(key, String.valueOf(submitCharArray));
        // 将该用户加入排行榜
        String userIdStr = userId.toString();
        int questionNum = submitCharArray.length;
        List<ScoreInfo.SingleQuestionScore> list;
        String userScoreKey = CONTEST_SCORE_PREFIX + contestId;
        // 加锁
        while (BooleanUtil.isFalse(stringRedisTemplate.opsForValue().setIfAbsent(String.format(CONTEST_SCORE_LOCK_PREFIX, contestId), "lock", CONTEST_SCORE_LOCK_TIME_OUT, CONTEST_SCORE_LOCK_TIME_UNIT))) {
        }
        // 本次比赛第一次提交
        if (!stringRedisTemplate.opsForHash().hasKey(userScoreKey, userIdStr)) {
            // 设置用户默认空的详细分数
            list = ScoreInfo.initEmptyQuestionScores(questionNum);
            // 提交过的用户需要加入到排行榜中
            // 加入排行榜中
            stringRedisTemplate.opsForZSet().add(ContestRedisConstant.CONTEST_SCOREBOARD_PREFIX + contestId, JSONUtil.toJsonStr(BeanUtil.copyProperties(userFeignClient.basicInfoById(userId), UserSimpleInfo.class)), 0);
        } else {
            list = JSONUtil.toList((String) stringRedisTemplate.opsForHash().get(userScoreKey, userIdStr), ScoreInfo.SingleQuestionScore.class);
        }
        list.get(questionIndex).setSubmitNum(list.get(questionIndex).getSubmitNum() + 1);
        // 设置提交过了
        list.get(questionIndex).setSubmit(true);
        stringRedisTemplate.opsForHash().put(userScoreKey, userIdStr, JSONUtil.toJsonStr(list));
        stringRedisTemplate.delete(String.format(CONTEST_SCORE_LOCK_PREFIX, contestId));
        return true;
    }

    private Page<ContestListVO> assemblePageResult(Page<OjContest> pageQueryResult) {
        Date now = new Date();
        return PageUtil.buildPage(
                pageQueryResult,
                pageQueryResult.getRecords().stream().map(r -> {
                    ContestListVO contestListVO = BeanUtil.copyProperties(r, ContestListVO.class);
                    contestListVO.setIsEnd(contestListVO.getEndTime().before(now));
                    contestListVO.setIsBegin(contestListVO.getBeginTime().before(now));
                    return contestListVO;
                }).collect(Collectors.toList()));
    }

    @Override
    public Page<ContestListVO> pageQueryContest(Long pageNum, Long pageSize) {
        Page<OjContest> pageQueryResult = page(new Page<>(pageNum, pageSize));
        return assemblePageResult(pageQueryResult);
    }

    @Override
    public Page<ContestListVO> pageQueryMineContest(Long userId, Long pageNum, Long pageSize) {
        Page<OjContest> pageQueryResult = page(new Page<>(pageNum, pageSize), new LambdaQueryWrapper<OjContest>().eq(OjContest::getHolderId, userId));
        return assemblePageResult(pageQueryResult);
    }

    @Override
    public Page<ContestListVO> pageQueryJoinedContest(Long userId, Long pageNum, Long pageSize) {
        List<Long> contestIds = stringRedisTemplate.opsForList().range(CONTEST_USER_JOIN_PREFIX + userId, (pageNum - 1) * pageSize, (pageNum - 1) * pageSize + pageSize).stream().map(Long::parseLong).collect(Collectors.toList());
        if (contestIds.isEmpty()) {
            return new Page<>(pageNum, pageSize);
        }
        Page<OjContest> pageQueryResult = page(new Page<>(pageNum, pageSize), new LambdaQueryWrapper<OjContest>().in(OjContest::getId, contestIds));
        return assemblePageResult(pageQueryResult);
    }

    @Override
    public ContestEditVO queryContestEditById(Long userId, Long contestId) {
        OjContest contest = getById(contestId);
        AuthorizationUtil.isLoginUserResource(userId, contest.getHolderId());
        ContestEditVO contestEditVO = BeanUtil.copyProperties(contest, ContestEditVO.class);
        List<OjQuestionBindContest> ojQuestionBindContests = ojQuestionBindContestService.listQuestionInfoByContestId(contestId);
        if (ojQuestionBindContests.isEmpty()) {
            contestEditVO.setQuestions(Collections.emptyList());
        } else {
            // 组装比赛题目
            List<QuestionBankContestEditQuestionVO> questions = questionFeignClient.assembleQuestion(ojQuestionBindContests.stream().map(OjQuestionBindContest::getQuestionId).collect(Collectors.toList()));
            for (int i = 0; i < questions.size(); i++) {
                questions.get(i).setMagnification(ojQuestionBindContests.get(i).getMagnification());
            }
            contestEditVO.setQuestions(questions);
        }
        return contestEditVO;
    }

    @Override
    public boolean updateContestBasicInfo(Long userId, ContestBasicInfoDTO contestBasicInfoDTO) {
        AuthorizationUtil.isLoginUserResource(userId, getById(contestBasicInfoDTO.getId()).getHolderId());
        OjContest ojContest = BeanUtil.copyProperties(contestBasicInfoDTO, OjContest.class);
        // 原定比赛已经开始
        if (ojContest.getBeginTime().before(new Date())) {
            // 不允许更改比赛的赛制，也不允许修改比赛开始时间
            if (!ojContest.getType().equals(contestBasicInfoDTO.getType()) || !ojContest.getBeginTime().equals(contestBasicInfoDTO.getBeginTime())) {
                throw new ZhimaOjException(CONTEST_HAS_BEGIN_NOT_ALLOW_UPDATE_TIME);
            }
        }
        ojContest.setDuration(DateUtil.between(ojContest.getBeginTime(), ojContest.getEndTime(), DateUnit.MINUTE));
        return updateById(ojContest);
    }

    @Override
    public boolean updateQuestionNum(Long contestId, boolean increase) {
        return ojContestMapper.updateQuestionNum(contestId, increase);
    }

    @Override
    public Integer getMagnification(Long contestId, Integer questionIndex) {
        return ojQuestionBindContestService.lambdaQuery().eq(OjQuestionBindContest::getContestId, contestId).eq(OjQuestionBindContest::getQuestionIndex, questionIndex).select(OjQuestionBindContest::getMagnification).one().getMagnification();
    }

}
